package org.example.order.bootstrap.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.sql.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

@Configuration
@MapperScan({"org.example.order.infrastructure.dao"})
public class ApplicationConfiguration {
    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CURRENT_ZONE_ID = ZoneId.systemDefault();

    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager manager = new DataSourceTransactionManager();
        manager.setDataSource(dataSource);
        return manager;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setConfiguration(configuration());
        return bean;
    }

    private org.apache.ibatis.session.Configuration configuration() {
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeHandlerRegistry().register(LocalDateTime.class, new BaseTypeHandler<LocalDateTime>() {
            @Override
            public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) throws SQLException {
                ps.setObject(i, parameter);
            }

            @Override
            public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
                return toLocalDateTime(rs.getObject(columnName));
            }

            @Override
            public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
                return toLocalDateTime(rs.getObject(columnIndex));
            }

            @Override
            public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
                return toLocalDateTime(cs.getObject(columnIndex));
            }

            private LocalDateTime toLocalDateTime(Object ts) throws SQLFeatureNotSupportedException {
                if (ts == null) {
                    return null;
                }
                if (ts instanceof Timestamp) {
                    return ((Timestamp) ts).toLocalDateTime();
                } else if (ts instanceof Date) {
                    return ((Date) ts).toInstant().atZone(CURRENT_ZONE_ID).toLocalDateTime();
                } else {
                    throw new SQLFeatureNotSupportedException();
                }
            }
        });
        configuration.getTypeHandlerRegistry().register(LocalDate.class, new BaseTypeHandler<LocalDate>() {
            @Override
            public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType) throws SQLException {
                ps.setObject(i, parameter);
            }

            @Override
            public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
                return toLocalDate(rs.getObject(columnName));
            }

            @Override
            public LocalDate getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
                return toLocalDate(rs.getObject(columnIndex));
            }

            @Override
            public LocalDate getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
                return toLocalDate(cs.getObject(columnIndex));
            }

            private LocalDate toLocalDate(Object ts) throws SQLFeatureNotSupportedException {
                if (ts == null) {
                    return null;
                }
                if (ts instanceof Timestamp) {
                    return ((Timestamp) ts).toLocalDateTime().toLocalDate();
                } else if (ts instanceof Date) {
                    return ((Date) ts).toLocalDate();
                } else {
                    throw new SQLFeatureNotSupportedException();
                }
            }
        });
        return configuration;
    }

    @Bean
    public ObjectMapper objectMapper() {
        SimpleModule javaTimeModule = new SimpleModule();
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DATE_FORMATTER));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATETIME_FORMATTER));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATETIME_FORMATTER));
        return new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .registerModule(javaTimeModule);
    }
}
